/*
 * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.eclipse.imagen.media.serialize;

import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.Arc2D;
import java.awt.geom.Area;
import java.awt.geom.CubicCurve2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.PathIterator;
import java.awt.geom.QuadCurve2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/**
 * This class is a serializable proxy for a Shape.
 *
 * @since 1.1
 */
public class ShapeState extends SerializableStateImpl {
    private static final int SHAPE_UNKNOWN = 0;
    private static final int SHAPE_AREA = 1;
    private static final int SHAPE_ARC_DOUBLE = 2;
    private static final int SHAPE_ARC_FLOAT = 3;
    private static final int SHAPE_CUBICCURVE_DOUBLE = 4;
    private static final int SHAPE_CUBICCURVE_FLOAT = 5;
    private static final int SHAPE_ELLIPSE_DOUBLE = 6;
    private static final int SHAPE_ELLIPSE_FLOAT = 7;
    private static final int SHAPE_GENERALPATH = 8;
    private static final int SHAPE_LINE_DOUBLE = 9;
    private static final int SHAPE_LINE_FLOAT = 10;
    private static final int SHAPE_QUADCURVE_DOUBLE = 11;
    private static final int SHAPE_QUADCURVE_FLOAT = 12;
    private static final int SHAPE_ROUNDRECTANGLE_DOUBLE = 13;
    private static final int SHAPE_ROUNDRECTANGLE_FLOAT = 14;
    private static final int SHAPE_RECTANGLE_DOUBLE = 15;
    private static final int SHAPE_RECTANGLE_FLOAT = 16;

    public static Class[] getSupportedClasses() {
        return new Class[] {Shape.class};
    }

    /**
     * Constructs a <code>ShapeState</code> from a <code>Shape</code>.
     *
     * @param c The class of the object to be serialized.
     * @param o The <code>Shape</code> to be serialized.
     * @param h The <code>RenderingHints</code> (ignored).
     */
    public ShapeState(Class c, Object o, RenderingHints h) {
        super(c, o, h);
    }

    /**
     * Serialize the <code>ShapeState</code>.
     *
     * @param out The <code>ObjectOutputStream</code>.
     */
    private void writeObject(ObjectOutputStream out) throws IOException {
        boolean serializable = false;
        Object object = theObject;

        // if the specific Shape is Serializable, then write itself;
        // if the specific Shape has a proxy itself, use that proxy;
        // for the regular Shapes, such as Arc, Ellipse and etc, write
        // the parameters;
        // for an Area, write the path and recover the Area;
        // for the instance of GeneralPath or unknown Shape, write the path;
        //
        if (object instanceof Serializable) serializable = true;

        // deal with Serializable Shape such as Polygon and Rectangle or
        // the Shape has its own proxy.
        //
        out.writeBoolean(serializable);
        if (serializable) {
            out.writeObject(object);
            return;
        }

        Object dataArray = null;
        Object otherData = null;
        int type = SHAPE_UNKNOWN;

        // deal with the regular Shapes
        if (theObject instanceof Area) type = SHAPE_AREA;
        else if (theObject instanceof Arc2D.Double) {
            Arc2D.Double ad = (Arc2D.Double) theObject;
            dataArray = new double[] {ad.x, ad.y, ad.width, ad.height, ad.start, ad.extent};
            type = SHAPE_ARC_DOUBLE;
            otherData = new Integer(ad.getArcType());
        } else if (theObject instanceof Arc2D.Float) {
            Arc2D.Float af = (Arc2D.Float) theObject;
            dataArray = new float[] {af.x, af.y, af.width, af.height, af.start, af.extent};
            type = SHAPE_ARC_FLOAT;
            otherData = new Integer(af.getArcType());
        } else if (theObject instanceof CubicCurve2D.Double) {
            CubicCurve2D.Double cd = (CubicCurve2D.Double) theObject;
            dataArray = new double[] {cd.x1, cd.y1, cd.ctrlx1, cd.ctrly1, cd.ctrlx2, cd.ctrly2, cd.x2, cd.y2};
            type = SHAPE_CUBICCURVE_DOUBLE;
        } else if (theObject instanceof CubicCurve2D.Float) {
            CubicCurve2D.Float cf = (CubicCurve2D.Float) theObject;
            dataArray = new float[] {cf.x1, cf.y1, cf.ctrlx1, cf.ctrly1, cf.ctrlx2, cf.ctrly2, cf.x2, cf.y2};
            type = SHAPE_CUBICCURVE_FLOAT;
        } else if (theObject instanceof Ellipse2D.Double) {
            Ellipse2D.Double ed = (Ellipse2D.Double) theObject;
            dataArray = new double[] {ed.x, ed.y, ed.width, ed.height};
            type = SHAPE_ELLIPSE_DOUBLE;
        } else if (theObject instanceof Ellipse2D.Float) {
            Ellipse2D.Float ef = (Ellipse2D.Float) theObject;
            dataArray = new float[] {ef.x, ef.y, ef.width, ef.height};
            type = SHAPE_ELLIPSE_FLOAT;
        } else if (theObject instanceof GeneralPath) type = SHAPE_GENERALPATH;
        else if (theObject instanceof Line2D.Double) {
            Line2D.Double ld = (Line2D.Double) theObject;
            dataArray = new double[] {ld.x1, ld.y1, ld.x2, ld.y2};
            type = SHAPE_LINE_DOUBLE;
        } else if (theObject instanceof Line2D.Float) {
            Line2D.Float lf = (Line2D.Float) theObject;
            dataArray = new float[] {lf.x1, lf.y1, lf.x2, lf.y2};
            type = SHAPE_LINE_FLOAT;
        } else if (theObject instanceof QuadCurve2D.Double) {
            QuadCurve2D.Double qd = (QuadCurve2D.Double) theObject;
            dataArray = new double[] {qd.x1, qd.y1, qd.ctrlx, qd.ctrly, qd.x2, qd.y2};
            type = SHAPE_QUADCURVE_DOUBLE;
        } else if (theObject instanceof QuadCurve2D.Float) {
            QuadCurve2D.Float qf = (QuadCurve2D.Float) theObject;
            dataArray = new float[] {qf.x1, qf.y1, qf.ctrlx, qf.ctrly, qf.x2, qf.y2};
            type = SHAPE_QUADCURVE_FLOAT;
        } else if (theObject instanceof RoundRectangle2D.Double) {
            RoundRectangle2D.Double rrd = (RoundRectangle2D.Double) theObject;
            dataArray = new double[] {rrd.x, rrd.y, rrd.width, rrd.height, rrd.arcwidth, rrd.archeight};
            type = SHAPE_ROUNDRECTANGLE_DOUBLE;
        } else if (theObject instanceof RoundRectangle2D.Float) {
            RoundRectangle2D.Float rrf = (RoundRectangle2D.Float) theObject;
            dataArray = new float[] {rrf.x, rrf.y, rrf.width, rrf.height, rrf.arcwidth, rrf.archeight};
            type = SHAPE_ROUNDRECTANGLE_FLOAT;
        } else if (theObject instanceof Rectangle2D.Double) {
            Rectangle2D.Double rd = (Rectangle2D.Double) theObject;
            dataArray = new double[] {rd.x, rd.y, rd.width, rd.height};
            type = SHAPE_RECTANGLE_DOUBLE;
        } else if (theObject instanceof Rectangle2D.Float) {
            Rectangle2D.Float rf = (Rectangle2D.Float) theObject;
            dataArray = new float[] {rf.x, rf.y, rf.width, rf.height};
            type = SHAPE_RECTANGLE_FLOAT;
        }
        // write Shape belonging to : GenrealPath, Area and unknown classes
        out.writeInt(type);
        if (dataArray != null) {
            out.writeObject(dataArray);
            if (otherData != null) out.writeObject(otherData);
            return;
        }

        PathIterator pathIterator = ((Shape) theObject).getPathIterator(null);

        // obtain and write the winding rule
        int rule = pathIterator.getWindingRule();
        out.writeInt(rule);

        float[] coordinates = new float[6];

        // iteratively write isDone, segment type and segment coordinates
        boolean isDone = pathIterator.isDone();
        while (!isDone) {
            int segmentType = pathIterator.currentSegment(coordinates);
            out.writeBoolean(isDone);
            out.writeInt(segmentType);
            for (int i = 0; i < 6; i++) out.writeFloat(coordinates[i]);
            pathIterator.next();
            isDone = pathIterator.isDone();
        }
        out.writeBoolean(isDone);
    }

    /**
     * Deserialize the <code>ShapeState</code>.
     *
     * @param out The <code>ObjectInputStream</code>.
     */
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {

        boolean serializable = in.readBoolean();

        // if Serializable or has a specific proxy
        if (serializable) {
            theObject = in.readObject();
            return;
        }

        // read the type of the wrapped Shape
        int type = in.readInt();

        // for regular shapes, read the parameters and recover it
        switch (type) {
            case SHAPE_ARC_DOUBLE: {
                double[] data = (double[]) in.readObject();
                int arcType = ((Integer) in.readObject()).intValue();
                theObject = new Arc2D.Double(data[0], data[1], data[2], data[3], data[4], data[5], arcType);
                return;
            }
            case SHAPE_ARC_FLOAT: {
                float[] data = (float[]) in.readObject();
                int arcType = ((Integer) in.readObject()).intValue();
                theObject = new Arc2D.Float(data[0], data[1], data[2], data[3], data[4], data[5], arcType);
                return;
            }
            case SHAPE_CUBICCURVE_DOUBLE: {
                double[] data = (double[]) in.readObject();
                theObject =
                        new CubicCurve2D.Double(data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7]);
                return;
            }
            case SHAPE_CUBICCURVE_FLOAT: {
                float[] data = (float[]) in.readObject();
                theObject =
                        new CubicCurve2D.Float(data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7]);
                return;
            }
            case SHAPE_ELLIPSE_DOUBLE: {
                double[] data = (double[]) in.readObject();
                theObject = new Ellipse2D.Double(data[0], data[1], data[2], data[3]);
                return;
            }
            case SHAPE_ELLIPSE_FLOAT: {
                float[] data = (float[]) in.readObject();
                theObject = new Ellipse2D.Float(data[0], data[1], data[2], data[3]);
                return;
            }

            case SHAPE_LINE_DOUBLE: {
                double[] data = (double[]) in.readObject();
                theObject = new Line2D.Double(data[0], data[1], data[2], data[3]);
                return;
            }
            case SHAPE_LINE_FLOAT: {
                float[] data = (float[]) in.readObject();
                theObject = new Line2D.Float(data[0], data[1], data[2], data[3]);
                return;
            }
            case SHAPE_QUADCURVE_DOUBLE: {
                double[] data = (double[]) in.readObject();
                theObject = new QuadCurve2D.Double(data[0], data[1], data[2], data[3], data[4], data[5]);
                return;
            }
            case SHAPE_QUADCURVE_FLOAT: {
                float[] data = (float[]) in.readObject();
                theObject = new QuadCurve2D.Float(data[0], data[1], data[2], data[3], data[4], data[5]);

                return;
            }
            case SHAPE_ROUNDRECTANGLE_DOUBLE: {
                double[] data = (double[]) in.readObject();
                theObject = new RoundRectangle2D.Double(data[0], data[1], data[2], data[3], data[4], data[5]);
                return;
            }
            case SHAPE_ROUNDRECTANGLE_FLOAT: {
                float[] data = (float[]) in.readObject();
                theObject = new RoundRectangle2D.Float(data[0], data[1], data[2], data[3], data[4], data[5]);
                return;
            }
            case SHAPE_RECTANGLE_DOUBLE: {
                double[] data = (double[]) in.readObject();
                theObject = new Rectangle2D.Double(data[0], data[1], data[2], data[3]);
                return;
            }
            case SHAPE_RECTANGLE_FLOAT: {
                float[] data = (float[]) in.readObject();
                theObject = new Rectangle2D.Float(data[0], data[1], data[2], data[3]);
                return;
            }
        }

        // read the winding rule
        int rule = in.readInt();

        GeneralPath path = new GeneralPath(rule);
        float[] coordinates = new float[6];

        // read the path
        while (!in.readBoolean()) {
            int segmentType = in.readInt();
            for (int i = 0; i < 6; i++) coordinates[i] = in.readFloat();

            switch (segmentType) {
                case PathIterator.SEG_MOVETO:
                    path.moveTo(coordinates[0], coordinates[1]);
                    break;

                case PathIterator.SEG_LINETO:
                    path.lineTo(coordinates[0], coordinates[1]);
                    break;

                case PathIterator.SEG_QUADTO:
                    path.quadTo(coordinates[0], coordinates[1], coordinates[2], coordinates[3]);
                    break;

                case PathIterator.SEG_CUBICTO:
                    path.curveTo(
                            coordinates[0],
                            coordinates[1],
                            coordinates[2],
                            coordinates[3],
                            coordinates[4],
                            coordinates[5]);
                    break;

                case PathIterator.SEG_CLOSE:
                    path.closePath();
                    break;
                default:
                    break;
            }
        }

        // recover Area instance
        switch (type) {
            case SHAPE_AREA:
                theObject = new Area(path);
                break;
            default:
                theObject = path;
                break;
        }
    }
}
